﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.IO;
using System.Reflection.Emit;
using Kokomo.Mirror.CliMetadata;

namespace Kokomo.PeInspector.Reflection.Disassembly
{
	public class MsilReader
	{
		private const byte TwoByteEscape = 0xFE;
		#region Opcodes
		private static IDictionary<ushort, OpCode> OpCodeTable { get; set; }
		private static void InitializeOpCodeTable()
		{
			List<OpCode> opcodes = typeof(OpCodes).GetStaticValues<OpCode>().ToList();
			opcodes.Remove(OpCodes.Prefix1);
			opcodes.Remove(OpCodes.Prefix2);
			opcodes.Remove(OpCodes.Prefix3);
			opcodes.Remove(OpCodes.Prefix4);
			opcodes.Remove(OpCodes.Prefix5);
			opcodes.Remove(OpCodes.Prefix6);
			opcodes.Remove(OpCodes.Prefix7);
			opcodes.Remove(OpCodes.Prefixref);
			OpCodeTable = opcodes.ToDictionary(o => (ushort)o.Value);
		}
		#endregion

		static MsilReader()
		{
			InitializeOpCodeTable();
		}

		public MethodBase Method { get; private set; }
		public MethodBody MethodBody { get; private set; }

		public MsilReader(
			MethodBase method,
			MsilReaderOptions options = MsilReaderOptions.None,
			MethodBody methodBody = null
			)
		{
			if (method == null) throw new ArgumentNullException("method");

			this.Method = method;
			this.Options = options;
			this._memberRefsOnly = options.HasFlag(MsilReaderOptions.MemberReferencesOnly);

			if (methodBody == null)
				methodBody = method.GetMethodBody();
			if (methodBody == null) throw new ArgumentException("Method has no body");
			this.MethodBody = methodBody;
			byte[] msilData = methodBody.GetILAsByteArray();
			this._bitReader = new BitReader(msilData);

			this.GenericTypeArguments = (method.DeclaringType == null) ? null : method.DeclaringType.GetGenericArguments();
			this.GenericMethodArguments =
				(method is MethodInfo)
				? method.GetGenericArguments()
				: null
				;
		}

		public override string ToString()
		{
			return string.Format("{0:X4}: OpCode={1}, Operand={2}",
				this.InstructionOffset,
				this.OpCode,
				this.Operand
				);
		}

		public MsilReaderOptions Options { get; private set; }
		private bool _memberRefsOnly;

		private BitReader _bitReader;

		public Type[] GenericTypeArguments { get; private set; }
		public Type[] GenericMethodArguments { get; private set; }

		public int InstructionOffset { get; private set; }
		public int InstructionLength { get; private set; }
		public OpCode OpCode { get; private set; }
		public object Operand { get; private set; }

		public bool MoveNext()
		{
			ushort opcodeValue = 0;

			BitReader reader = this._bitReader;

			if (!reader.IsEndOfStream)
			{
				this.InstructionOffset = reader.Position;

				OpCode opcode;
				opcodeValue = reader.ReadByte();
				//if (!OpCodeTable.TryGetValue(opcodeValue, out opcode))
				if (opcodeValue == TwoByteEscape)
				{
					opcodeValue <<= 8;
					opcodeValue |= reader.ReadByte();
				}
				if (!OpCodeTable.TryGetValue(opcodeValue, out opcode))
					throw new ArgumentException("Unknown opcode");

				// Check the operand
				object operand = null;
				if ((opcode.OperandType != OperandType.InlineNone))
				{
					operand = GetMsilOperand(opcode.OperandType);
				}

				this.OpCode = opcode;
				this.Operand = operand;
				this.InstructionLength = reader.Position - this.InstructionOffset;
			}
			else
			{
				return false;
			}

			return true;
		}

		//private short ReadInt16(ref int offset)
		//{
		//    byte[] ilData = this.MsilData;
		//    short i16 = (short)(
		//        ilData[offset++]
		//        | (ilData[offset++] << 8)
		//        )
		//        ;

		//    return i16;
		//}
		//private int ReadInt32(ref int offset)
		//{
		//    byte[] ilData = this.MsilData;
		//    int i32 =
		//        ilData[offset++]
		//        | (ilData[offset++] << 8)
		//        | (ilData[offset++] << 16)
		//        | (ilData[offset++] << 24)
		//        ;

		//    return i32;
		//}
		//private long ReadInt64(ref int offset)
		//{
		//    byte[] ilData = this.MsilData;
		//    long i64 =
		//        ilData[offset++]
		//        | (ilData[offset++] << 8)
		//        | (ilData[offset++] << 16)
		//        | (ilData[offset++] << 24)
		//        | (ilData[offset++] << 32)
		//        | (ilData[offset++] << 40)
		//        | (ilData[offset++] << 48)
		//        | (ilData[offset++] << 56)
		//        ;

		//    return i64;
		//}
		//private float ReadSingle(ref int offset)
		//{
		//    float num = BitConverter.ToSingle(this.MsilData, offset);
		//    offset += 4;
		//    return num;
		//}
		//private double ReadDouble(ref int offset)
		//{
		//    double num = BitConverter.ToDouble(this.MsilData, offset);
		//    offset += 8;
		//    return num;
		//}

		private static RuntimeTypeHandle GetTypeHandle(Type type) { return type.TypeHandle; }
		private object GetMsilOperand(OperandType operandType)
		{
			MethodBase method = this.Method;
			Module module = method.Module;

			BitReader reader = this._bitReader;

			int i32 = 0;

			bool isMetadataToken = false;
			object operand = null;

			switch (operandType)
			{
				case OperandType.InlineBrTarget:
					i32 = reader.ReadInt32();
					operand = new MsilBranchTarget(reader.Position + i32);
					break;
				case OperandType.InlineI:
					i32 = reader.ReadInt32();
					operand = i32;
					break;
				case OperandType.InlineField:
				case OperandType.InlineMethod:
				case OperandType.InlineTok:
				case OperandType.InlineType:
					i32 = reader.ReadInt32();
					isMetadataToken = true;
					break;
				case OperandType.InlineI8:
					long i64 = reader.ReadInt64();
					operand = i64;
					break;
				case OperandType.InlineNone:
					break;
				case OperandType.InlineR:
					double r64 = reader.ReadDouble();
					operand = r64;
					break;
				case OperandType.InlineSig:
					i32 = reader.ReadInt32();
					byte[] targetSignature = module.ResolveSignature(i32);
					operand = targetSignature;
					break;
				case OperandType.InlineString:
					i32 = reader.ReadInt32();
					string str = module.ResolveString(i32);
					operand = str;
					break;
				case OperandType.InlineSwitch:
					{
						int targetCount = reader.ReadInt32();
						int[] relativeOffsets = new int[targetCount];
						for (int i = 0; i < targetCount; i++)
						{
							int relativeOffset = reader.ReadInt32();
							relativeOffsets[i] = relativeOffset;
						}

						int baseOffset = reader.Position;
						MsilBranchTarget[] targets = new MsilBranchTarget[targetCount];
						for (int i = 0; i < targetCount; i++)
						{
							targets[i] = new MsilBranchTarget(relativeOffsets[i] + baseOffset);
						}
						operand = targets;
					}
					break;
				case OperandType.InlineVar:
					{
						int localIndex = reader.ReadInt16();
						operand = localIndex;
					}
					break;
				case OperandType.ShortInlineI:
					sbyte b8 = reader.ReadSByte();
					operand = b8;
					break;
				case OperandType.ShortInlineBrTarget:
					sbyte sb8 = reader.ReadSByte();
					operand = new MsilBranchTarget(reader.Position + sb8);
					break;
				case OperandType.ShortInlineVar:
					{
						byte localIndex = reader.ReadByte();
						operand = localIndex;
						break;
					}
				//short i16 = reader.ReadInt8();
				//value = i16;
				//break;
				case OperandType.ShortInlineR:
					float r32 = reader.ReadSingle();
					operand = r32;
					break;
			}

			if (isMetadataToken)
			{
				if (this._memberRefsOnly)
				{
					operand = this.ResolveMemberAsReference(operandType, i32);
				}
				else
				{
					operand = this.ResolveMember(operandType, i32);
				}
			}

			return operand;
		}

		private MemberInfo ResolveMember(OperandType operandType, int token)
		{
			MemberInfo member = null;
			Module module = this.Method.Module;

			switch (operandType)
			{
				case OperandType.InlineField:
					member = module.ResolveField(token, this.GenericTypeArguments, this.GenericMethodArguments);
					break;
				case OperandType.InlineMethod:
					//RuntimeMethodHandle methodHandle = moduleHandle.ResolveMethodHandle(i32, genericTypeArgumentHandles, genericMethodArgumentHandles);
					member = module.ResolveMethod(token, this.GenericTypeArguments, this.GenericMethodArguments);
					break;
				case OperandType.InlineTok:
					member = module.ResolveMember(token, this.GenericTypeArguments, this.GenericMethodArguments);
					break;
				case OperandType.InlineType:
					member = module.ResolveType(token, this.GenericTypeArguments, this.GenericMethodArguments);
					break;
			}

			if (member == null) throw new ArgumentException();

			return member;
		}

		private static int _canonCount;

		private object ResolveMemberAsReference(OperandType operandType, int token)
		{
			ModuleMemberReference memberRef;
			if (CodedIndex.IsCanonicalToken(token))
			{
				memberRef = ModuleMemberReference.FromModuleToken(this.Method.Module, token);
			}
			else
			{
				_canonCount++;
				MemberInfo member = this.ResolveMember(operandType, token);
				if (!CodedIndex.IsNullToken(member.MetadataToken))
					memberRef = new ModuleMemberReference(member);
				else
					memberRef = ModuleMemberReference.FromModuleToken(this.Method.Module, token);
			}

			return memberRef;
		}
	}
}
